<script>
import { GlFriendlyWrap, GlLink, GlSprintf } from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { bodyWithFallBack } from 'ee/vue_shared/security_reports/components/helpers';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type';
import {
  SUPPORTING_MESSAGE_TYPES,
  VULNERABILITY_TRAINING_HEADING,
} from 'ee/vulnerabilities/constants';
import { s__, __ } from '~/locale';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DetailItem from './detail_item.vue';
import FalsePositiveAlert from './false_positive_alert.vue';
import VulnerabilityDetailSection from './vulnerability_detail_section.vue';
import VulnerabilityTraining from './vulnerability_training.vue';
import ExplainThisVulnerability from './explain_this_vulnerability.vue';

export default {
  name: 'VulnerabilityDetails',
  components: {
    CodeBlock,
    FalsePositiveAlert,
    GlLink,
    SeverityBadge,
    DetailItem,
    GlFriendlyWrap,
    GlSprintf,
    VulnerabilityDetailSection,
    VulnerabilityTraining,
    ExplainThisVulnerability,
  },
  directives: {
    SafeHtml,
  },
  mixins: [glFeatureFlagMixin()],
  inject: {
    projectFullPath: {
      default: '',
    },
  },
  props: {
    vulnerability: {
      type: Object,
      required: true,
    },
  },
  computed: {
    humanReadableReportType() {
      return convertReportType(this.vulnerability.reportType);
    },
    location() {
      return this.vulnerability.location || {};
    },
    stacktraceSnippet() {
      return this.vulnerability.stacktraceSnippet || '';
    },
    scanner() {
      return this.vulnerability.scanner || {};
    },
    fileText() {
      return (this.location.file || '') + (this.lineNumber ? `:${this.lineNumber}` : '');
    },
    fileUrl() {
      return (this.location.blobPath || '') + (this.lineNumber ? `#L${this.lineNumber}` : '');
    },
    isFalsePositive() {
      return Boolean(this.vulnerability.falsePositive);
    },
    lineNumber() {
      const { startLine: start, endLine: end } = this.location;
      return end > start ? `${start}-${end}` : start;
    },
    requestOrLocationUrl() {
      return this.vulnerability.request?.url || this.locationUrl;
    },
    locationUrl() {
      const { hostname, path } = this.location;
      return typeof hostname === 'string' && typeof path === 'string' ? `${hostname}${path}` : '';
    },
    scannerUrl() {
      return this.scanner.url || '';
    },
    scannerDetails() {
      if (this.scannerUrl) {
        return {
          component: 'GlLink',
          properties: {
            href: this.scannerUrl,
            target: '_blank',
          },
        };
      }

      return {
        component: 'span',
        properties: {},
      };
    },
    assertion() {
      return this.vulnerability.evidenceSource?.name;
    },
    recordedMessage() {
      return this.vulnerability?.supportingMessages?.find(
        (msg) => msg.name === SUPPORTING_MESSAGE_TYPES.RECORDED,
      )?.response;
    },
    constructedRequest() {
      return this.constructRequest(this.vulnerability.request);
    },
    constructedResponse() {
      return this.constructResponse(this.vulnerability.response);
    },
    constructedRecordedResponse() {
      return this.constructResponse(this.recordedMessage);
    },
    project() {
      return this.vulnerability.project;
    },
    requestData() {
      if (!this.vulnerability.request) {
        return [];
      }

      return [
        {
          label: __('%{labelStart}Sent request:%{labelEnd} %{headers}'),
          content: this.constructedRequest,
          isCode: true,
        },
      ].filter((x) => x.content);
    },
    responseData() {
      if (!this.vulnerability.response) {
        return [];
      }

      return [
        {
          label: __('%{labelStart}Actual response:%{labelEnd} %{headers}'),
          content: this.constructedResponse,
          isCode: true,
        },
      ].filter((x) => x.content);
    },
    recordedResponseData() {
      if (!this.recordedMessage) {
        return [];
      }

      return [
        {
          label: __('%{labelStart}Unmodified response:%{labelEnd} %{headers}'),
          content: this.constructedRecordedResponse,
          isCode: true,
        },
      ].filter((x) => x.content);
    },
    shouldShowLocation() {
      return (
        this.location.crashAddress ||
        this.location.crashState ||
        this.location.crashType ||
        this.location.stacktraceSnippet ||
        this.location.file ||
        this.location.image ||
        this.location.operatingSystem
      );
    },
    hasRequest() {
      return Boolean(this.requestData.length);
    },
    hasResponse() {
      return Boolean(this.responseData.length);
    },
    hasRecordedResponse() {
      return Boolean(this.recordedResponseData.length);
    },
    hasResponses() {
      return Boolean(this.hasResponse || this.hasRecordedResponse);
    },
    shouldShowTraining() {
      return this.vulnerability.identifiers?.length > 0 && Boolean(this.projectFullPath);
    },
    vulnerableMethod() {
      // `location.method` is provided by the REST API and `location.vulnerableMethod` by GraphQL
      // once https://gitlab.com/groups/gitlab-org/-/epics/3657 and https://gitlab.com/groups/gitlab-org/-/epics/8054 have been completed we can remove the support for `location.method`
      return this.location.method || this.location.vulnerableMethod;
    },
    shouldShowExplainThisVulnerability() {
      return (
        this.glFeatures.explainVulnerability && this.vulnerability.reportType === REPORT_TYPE_SAST
      );
    },
  },
  methods: {
    getHeadersAsCodeBlockLines(headers) {
      return Array.isArray(headers)
        ? headers.map(({ name, value }) => `${name}: ${value}`).join('\n')
        : '';
    },
    constructResponse(response) {
      const { body, statusCode, reasonPhrase = '', headers = [] } = response;
      const headerLines = this.getHeadersAsCodeBlockLines(headers);

      return statusCode && headerLines
        ? [`${statusCode} ${reasonPhrase}\n`, headerLines, '\n\n', bodyWithFallBack(body)].join('')
        : '';
    },
    constructRequest(request) {
      const { body, method, url, headers = [] } = request;
      const headerLines = this.getHeadersAsCodeBlockLines(headers);

      return method && url && headerLines
        ? [`${method} ${url}\n`, headerLines, '\n\n', bodyWithFallBack(body)].join('')
        : '';
    },
  },
  i18n: {
    requestResponse: s__('Vulnerability|Request/Response'),
    unmodifiedResponse: s__(
      'Vulnerability|The unmodified response is the original response that had no mutations done to the request',
    ),
    actualResponse: s__(
      'Vulnerability|Actual received response is the one received when this fault was detected',
    ),
  },
  VULNERABILITY_TRAINING_HEADING,
};
</script>

<template>
  <div data-qa-selector="vulnerability_details">
    <false-positive-alert v-if="isFalsePositive" class="gl-mb-5" />

    <div class="md">
      <h1
        v-if="vulnerability.title"
        class="gl-mb-3! border-bottom-0"
        data-testid="title"
        data-qa-selector="vulnerability_title"
      >
        {{ vulnerability.title }}
      </h1>
      <h3 class="mt-0">{{ __('Description') }}</h3>
      <p
        v-if="vulnerability.descriptionHtml"
        v-safe-html="vulnerability.descriptionHtml"
        data-testid="description"
        data-qa-selector="vulnerability_description"
      ></p>
      <p v-else data-testid="description" data-qa-selector="vulnerability_description">
        {{ vulnerability.description }}
      </p>

      <ul>
        <detail-item :sprintf-message="__('%{labelStart}Severity:%{labelEnd} %{severity}')">
          <severity-badge :severity="vulnerability.severity" class="gl-display-inline ml-1" />
        </detail-item>

        <detail-item
          v-if="project"
          :sprintf-message="__('%{labelStart}Project:%{labelEnd} %{project}')"
        >
          <gl-link :href="project.fullPath" target="_blank">{{ project.fullName }}</gl-link>
        </detail-item>

        <detail-item :sprintf-message="__('%{labelStart}Tool:%{labelEnd} %{reportType}')">{{
          humanReadableReportType
        }}</detail-item>
        <detail-item
          v-if="scanner.name"
          :sprintf-message="__('%{labelStart}Scanner:%{labelEnd} %{scanner}')"
        >
          <component
            :is="scannerDetails.component"
            v-bind="scannerDetails.properties"
            data-testid="scannerSafeLink"
          >
            <gl-sprintf
              v-if="scanner.version"
              :message="s__('Vulnerability|%{scannerName} (version %{scannerVersion})')"
            >
              <template #scannerName>{{ scanner.name }}</template>
              <template #scannerVersion>{{ scanner.version }}</template>
            </gl-sprintf>
            <template v-else>{{ scanner.name }}</template>
          </component>
        </detail-item>
        <detail-item
          v-if="location.class"
          :sprintf-message="__('%{labelStart}Class:%{labelEnd} %{class}')"
          >{{ location.class }}
        </detail-item>
        <detail-item
          v-if="vulnerableMethod"
          :sprintf-message="__('%{labelStart}Method:%{labelEnd} %{method}')"
        >
          <code>{{ vulnerableMethod }}</code>
        </detail-item>
        <detail-item
          v-if="requestOrLocationUrl"
          :sprintf-message="__('%{labelStart}URL:%{labelEnd} %{url}')"
        >
          <gl-link :href="requestOrLocationUrl" target="_blank">
            <gl-friendly-wrap :text="requestOrLocationUrl" />
          </gl-link>
        </detail-item>
        <detail-item
          v-if="vulnerability.evidence"
          :sprintf-message="__('%{labelStart}Evidence:%{labelEnd} %{evidence}')"
          >{{ vulnerability.evidence }}
        </detail-item>
      </ul>

      <template v-if="shouldShowLocation">
        <h3>{{ __('Location') }}</h3>
        <ul>
          <detail-item
            v-if="location.image"
            :sprintf-message="__('%{labelStart}Image:%{labelEnd} %{image}')"
            >{{ location.image }}
          </detail-item>
          <detail-item
            v-if="location.operatingSystem"
            :sprintf-message="__('%{labelStart}Namespace:%{labelEnd} %{namespace}')"
            >{{ location.operatingSystem }}
          </detail-item>
          <detail-item
            v-if="location.file"
            :sprintf-message="__('%{labelStart}File:%{labelEnd} %{file}')"
          >
            <gl-link :href="fileUrl" target="_blank">{{ fileText }}</gl-link>
          </detail-item>
          <detail-item
            v-if="location.crashAddress"
            :sprintf-message="__('%{labelStart}Crash Address:%{labelEnd} %{crash_address}')"
            >{{ location.crashAddress }}
          </detail-item>
          <detail-item
            v-if="location.crashType"
            :sprintf-message="__('%{labelStart}Crash Type:%{labelEnd} %{crash_type}')"
            >{{ location.crashType }}
          </detail-item>
          <detail-item
            v-if="location.crashState"
            :sprintf-message="__('%{labelStart}Crash State:%{labelEnd} %{crash_state}')"
            >{{ location.crashState }}
          </detail-item>
          <detail-item
            v-if="location.stacktraceSnippet"
            :sprintf-message="__('%{labelStart}Crash State:%{labelEnd} %{stacktrace_snippet}')"
          >
            <code-block :code="location.stacktraceSnippet" max-height="225px" />
          </detail-item>
        </ul>
      </template>

      <template v-if="vulnerability.links && vulnerability.links.length">
        <h3>{{ __('Links') }}</h3>
        <ul>
          <li
            v-for="(link, index) in vulnerability.links"
            :key="`${index}:${link.url}`"
            class="gl-ml-0! gl-list-style-position-inside"
          >
            <gl-link
              :href="link.url"
              data-testid="link"
              target="_blank"
              :aria-label="__('Third Party Advisory Link')"
              :title="link.url"
            >
              {{ link.name || link.url }}
            </gl-link>
          </li>
        </ul>
      </template>

      <template v-if="vulnerability.identifiers && vulnerability.identifiers.length">
        <h3>{{ __('Identifiers') }}</h3>
        <ul>
          <li
            v-for="(identifier, index) in vulnerability.identifiers"
            :key="`${index}:${identifier.url}`"
            class="gl-ml-0! gl-list-style-position-inside"
          >
            <component
              :is="identifier.url ? 'gl-link' : 'span'"
              v-bind="identifier.url && { href: identifier.url, target: '_blank' }"
              data-testid="identifier"
            >
              {{ identifier.name }}
            </component>
          </li>
        </ul>
      </template>

      <vulnerability-detail-section
        v-if="hasRequest"
        data-testid="request"
        :list-data="requestData"
        :heading="$options.i18n.requestResponse"
      />

      <div v-if="hasResponses" class="row">
        <vulnerability-detail-section
          v-if="hasRecordedResponse"
          data-testid="recorded-response"
          :class="hasResponse ? 'col-6' : 'col'"
          :list-data="recordedResponseData"
          :icon-title="$options.i18n.unmodifiedResponse"
        />

        <vulnerability-detail-section
          v-if="hasResponse"
          data-testid="response"
          :class="hasRecordedResponse ? 'col-6' : 'col'"
          :list-data="responseData"
          :icon-title="$options.i18n.actualResponse"
        />
      </div>

      <template v-if="assertion">
        <h3>{{ s__('Vulnerability|Additional Info') }}</h3>
        <ul>
          <detail-item :sprintf-message="__('%{labelStart}Assert:%{labelEnd} %{assertion}')">
            {{ assertion }}
          </detail-item>
        </ul>
      </template>

      <template v-if="vulnerability.assets && vulnerability.assets.length">
        <h3>{{ s__('Vulnerability|Reproduction Assets') }}</h3>
        <ul>
          <li
            v-for="(asset, index) in vulnerability.assets"
            :key="`${index}:${asset.url}`"
            class="gl-ml-0! gl-list-style-position-inside"
          >
            <component
              :is="asset.url ? 'gl-link' : 'span'"
              v-bind="asset.url && { href: asset.url, target: '_blank' }"
              data-testid="asset"
            >
              {{ asset.name }}
            </component>
          </li>
        </ul>
      </template>

      <vulnerability-training
        v-if="shouldShowTraining"
        :project-full-path="projectFullPath"
        :identifiers="vulnerability.identifiers"
        :file="location.file"
        class="gl-mb-5"
      >
        <template #header>
          <h3>{{ $options.VULNERABILITY_TRAINING_HEADING.title }}</h3>
        </template>
      </vulnerability-training>
      <explain-this-vulnerability
        v-if="shouldShowExplainThisVulnerability"
        :vulnerability="vulnerability"
      />
    </div>
  </div>
</template>
